package match

import (
	"fmt"
	"github.com/jmespath/go-jmespath"
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"k8s.io/api/admission/v1beta1"
	"reflect"
	"regexp"
	"strings"
)

const (
	Deny    = "deny"
	Allow   = "allow"
	Enabled = "enabled"
)

type selOperator string

const (
	In           selOperator = "In"
	NotIn        selOperator = "NotIn"
	Exists       selOperator = "Exists"
	DoesNotExist selOperator = "DoesNotExist"
)

const (
	scopeCluster    = "Cluster"
	scopeNamespaced = "Namespaced"
	scopeAny        = "*"
)

const (
	mustExist    = "MustExist"
	mustNotExist = "MustNotExist"
)

func MatchCompatibility(data common.ARFields, match v1alpha1.Match) bool {
	if match.IsEmpty() {
		return true
	}

	var validationFuncs []func(data common.ARFields, match v1alpha1.Match) (matches bool, reason string)
	validationFuncs = append(validationFuncs,
		doesOperationsMatches,
		doesScopeMatches,
		doesUserInfoMatches,
		doesKindsMatches,
		doesNameMatches,
		doesNamespaceSelectorMatches,
		doesIncludeExcludeNssMatch,
		doesAnnotationSelectorMatches,
		doesLabelSelectorMatches,
		doesMatchConditionsMatches,
	)
	var matches bool
	var reason string
	for _, f := range validationFuncs {
		matches, reason = f(data, match)

		if !matches {
			obj := getObjectToValidate(data)
			var name = getAdmissionReviewResourceName(obj)
			logs.Debugf("[NotMatched] resource kind %v name %v"+
				" due to %s", data.Kind.Kind, name, reason)
			return false
		}
	}

	return true
}

func getAdmissionReviewResourceName(obj map[string]interface{}) (name string) {
	metadata, _ := obj["metadata"].(map[string]interface{})
	if metadata["name"] != nil {
		name = metadata["name"].(string)
	} else if metadata["generateName"] != nil {
		name = metadata["generateName"].(string)
	} else {
		name = "config_name_was_not_found"
	}
	return name
}

func getObjectToValidate(data common.ARFields) map[string]interface{} {
	switch data.Operation {
	case v1beta1.Create:
		return data.Object
	case v1beta1.Update:
		return data.Object
	case v1beta1.Delete:
		return data.OldObject
	}

	logs.Errorf("wrong operation")
	return nil
}

func doesOperationsMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	mOperation := match.Operations
	arOperation := fields.Operation

	if len(mOperation) == 0 {
		return true, ""
	}

	for _, op := range mOperation {
		if v1beta1.Operation(op) == arOperation {
			return true, ""
		}
	}
	return false, "request.operation"
}

func doesScopeMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	mScope := match.Scope
	if mScope == scopeAny || mScope == "" {
		return true, ""
	}
	var isNs bool
	isNs = isNamespaced(fields)
	if (isNs && mScope == scopeNamespaced) || (!isNs && mScope == scopeCluster) {
		return true, ""
	} else {
		return false, "request.namespace"
	}
}

func doesUserInfoMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	if reflect.DeepEqual(match.UserInfo, v1alpha1.UserInfo{}) {
		return true, ""
	}
	mUsername := match.UserInfo.Username
	arUsername := fields.UserInfo.Username
	if mUsername == "" {
		mUsername = match.UserInfo.UsernameRegex
		b, err := regexp.Match(mUsername, []byte(arUsername))
		if err != nil {
			return false, "wrong match.userInfo.usernameRegex: " + fmt.Sprint(err)
		}
		if !b {
			return false, "request.userInfo.username"
		}
		return b, ""
	} else {
		if match.UserInfo.Username != arUsername {
			return false, "request.userInfo.username"
		}

		return true, ""
	}
}

func doesKindsMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	mKinds := match.Kinds
	arKinds := fields.Kind

	kMatch, vMatch := false, false

	for _, kind := range mKinds {
		if !kMatch {
			kMatch = checkStringsMatchingWithSliceRegexp(kind.Kind, arKinds.Kind)
		}
		if !vMatch {
			vMatch = checkStringsMatchingWithSliceRegexp(kind.ApiGroups, arKinds.Group)
		}
	}
	if kMatch && vMatch {
		return true, ""
	}

	if !kMatch {
		reason = "request.kind.kind"
	}
	if !vMatch {
		reason = "request.kind.group"
	}

	return false, reason
}

func doesNameMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	reason = "request.oldObject.metadata.name"

	namePattern := match.Name
	if reflect.DeepEqual(namePattern, v1alpha1.Name{}) {
		return true, ""
	}
	object := getObjectToValidate(fields)
	name := getAdmissionReviewResourceName(object)

	if namePattern.Value != "" {
		if namePattern.Value == name {
			return true, ""
		} else {
			return false, reason
		}
	} else {
		matchRes, err := regexp.Match(namePattern.Regex, []byte(name))
		if err != nil {
			return false, "wrong match.name.regex: " + fmt.Sprint(err)
		}
		return matchRes, reason
	}
}

func doesIncludeExcludeNssMatch(fields common.ARFields, match v1alpha1.Match) (bool, string) {

	var matches = true
	var reason = ""

	var excluded = match.ExcludedNamespaces
	var included = match.IncludedNamespaces

	ns := fields.Namespace
	if ns == "" {
		return true, ""
	}

	if !reflect.DeepEqual(excluded, v1alpha1.Namespace{Values: nil, Regex: nil}) {
		matches, reason = doesNsNamesMatch(excluded, ns, false)
		if !matches {
			return matches, reason
		}
	} else if !reflect.DeepEqual(included, v1alpha1.Namespace{Values: nil, Regex: nil}) {
		matches, reason = doesNsNamesMatch(included, ns, true)
		if !matches {
			return matches, reason
		}
	}

	return true, ""
}

func doesNsNamesMatch(matchNs v1alpha1.Namespace, ns string, include bool) (bool, string) {

	var reason = ""
	var reasonPrefix string
	var match bool

	if include {
		reasonPrefix = "included "
	} else {
		reasonPrefix = "excluded "
	}

	if matchNs.Values != nil {
		match, reason = findNonRegexNsMatch(ns, matchNs.Values)
		return include == match, reasonPrefix + reason
	} else {
		match, reason = findRegexNsMatch(ns, matchNs.Regex)
		if reason != "namespace invalid regex" {
			return include == match, reasonPrefix + reason
		} else {
			return match, reason
		}

	}
}

func findNonRegexNsMatch(ns string, matchVal []string) (bool, string) {
	for _, matchNs := range matchVal {
		if ns == matchNs {
			return true, "namespace value"
		}
	}
	return false, "namespace value"
}

func findRegexNsMatch(ns string, matchVal []string) (bool, string) {
	for _, matchNs := range matchVal {
		match, err := regexp.Match(matchNs, []byte(ns))
		if err != nil {
			logs.Errorf("Namespace regex err: %v", err.Error())
			return false, "namespace invalid regex"
		}

		if match {
			return true, "namespace regex"
		}
	}
	return false, "namespace regex"
}

func doesNamespaceSelectorMatches(fields common.ARFields, match v1alpha1.Match) (bool, string) {
	if reflect.DeepEqual(match.NamespaceSelector, v1alpha1.NamespaceSelector{}) {
		return true, "namespace selector"
	}

	if fields.Namespace == "" { // non namespaced object
		logs.Debugf("[Skipped selector namespace] resource %s"+
			" escaped NamespaceSelectorMatch check because it's Cluster Object", fields.Kind.Kind)
		return true, "namespace selector"
	}

	namespaces := storage.Storage.GetNamespaces()
	for _, ns := range namespaces {
		if ns.ObjectMeta.Name == fields.Namespace {
			matches := doesMatchExpressionsMatch(ns.ObjectMeta.Labels, match.NamespaceSelector.MatchExpressions)
			if matches {
				return true, ""
			}
		}
	}
	return false, "namespace selector"
}

func doesAnnotationSelectorMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	if reflect.DeepEqual(match.AnnotationSelector, v1alpha1.AnnotationSelector{}) {
		return true, ""
	}
	matchExpr := match.AnnotationSelector.MatchExpressions

	object := getObjectToValidate(fields)
	annotations, _ := jmespath.Search("metadata.annotations", object)

	arAnnotations, ok := annotations.(map[string]interface{})
	if !ok {
		logs.Infof("perhaps the configuration being processed does not have annotations. " +
			" an error was detected when trying to extract the metadata.annotations section")
		return false, "request.metadata.annotations"
	}
	arAnnotationsStr := parseMapValuesToString(arAnnotations)

	return doesMatchExpressionsMatch(arAnnotationsStr, matchExpr), "annotation selector"
}

func doesLabelSelectorMatches(fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
	selector := match.LabelSelector
	object := getObjectToValidate(fields)

	if reflect.DeepEqual(selector, v1alpha1.LabelSelector{}) {
		return true, ""
	}

	labels, _ := jmespath.Search("metadata.labels", object)

	lbs, ok := labels.(map[string]interface{})
	if !ok {
		logs.Infof("perhaps the configuration being processed does not have labels. " +
			" an error was detected when trying to extract the metadata.labels section")
		return false, "wrong metadata.labels format"
	}
	arLabelsStr := parseMapValuesToString(lbs)

	return doesMatchExpressionsMatch(arLabelsStr, selector.MatchExpressions), "metadata.labels"
}

func doesMatchConditionsMatches(fields common.ARFields, match v1alpha1.Match) (bool, string) {
	if reflect.DeepEqual(match.MatchConditions, v1alpha1.MatchConditions{}) {
		return true, ""
	}

	var matches = true
	var reason = ""

	switch fields.Operation {
	case v1beta1.Create:
		if fields.Object != nil && len(match.MatchConditions.Object) != 0 {
			matches, reason = doesObjectSectionMatches(fields.Object, match.MatchConditions.Object)
			if !matches {
				return matches, reason
			}
		}
	case v1beta1.Delete:
		if fields.OldObject != nil && len(match.MatchConditions.OldObject) != 0 {
			matches, reason = doesObjectSectionMatches(fields.OldObject, match.MatchConditions.OldObject)
			if !matches {
				return matches, reason
			}
		}
	case v1beta1.Update:
		if fields.Object != nil && len(match.MatchConditions.Object) != 0 {
			matches, reason = doesObjectSectionMatches(fields.Object, match.MatchConditions.Object)
			if !matches {
				return matches, reason
			}
		}
		if fields.OldObject != nil && len(match.MatchConditions.OldObject) != 0 {
			matches, reason = doesObjectSectionMatches(fields.OldObject, match.MatchConditions.OldObject)
			if !matches {
				return matches, reason
			}
		}
	}
	return true, ""

}

func doesObjectSectionMatches(obj map[string]interface{}, matchObjs []v1alpha1.Obj) (bool, string) {
	if len(matchObjs) == 0 {
		return false, "match conditions doesn't have enough of arguments"
	}
	for _, mObj := range matchObjs {
		var path = strings.ReplaceAll(mObj.Path, "`", "'")
		res, err := jmespath.Search(path, obj)
		if err != nil {
			logs.Errorf("when processing MatchConditions jmespath: %s, value: %s an error was detected: %v ", mObj.Path, mObj.Condition, err)
			return false, "match conditions jmespath processing error"
		}
		switch v := res.(type) {
		case []interface{}:
			if (len(v) == 0 && mObj.Condition == mustNotExist) || (len(v) != 0 && mObj.Condition == mustExist) {
				continue
			}
			return false, "match conditions"
		case bool:
			if (v == true && mObj.Condition == mustExist) || (v == false && mObj.Condition == mustNotExist) {
				continue
			}
			return false, "match conditions"
		case interface{}:
			if (v == nil && mObj.Condition == mustNotExist) || (v != nil && mObj.Condition == mustExist) {
				continue
			}
			return false, "match conditions"
		default:
			if v == nil {
				if mObj.Condition == mustNotExist {
					continue
				}
				return false, "match conditions"
			}

			logs.Errorf("when processing MatchConditions jmespath: %s, value: %s an error occurred with unexpected type: %T! ", mObj.Path, mObj.Condition, v)
			return false, "match conditions"

		}

	}
	return true, ""
}
